04. Memory 与 RAG 工程实践
本章高频面试题
- 什么是短期记忆,什么是长期记忆?
- 为什么历史对话不能简单等同于记忆系统?
- 什么是 RAG?它和搜索、向量检索、知识库分别是什么关系?
- 为什么工业级 RAG 不能只靠纯向量检索?
- 什么是 Hybrid Search?什么是 rerank?常用 reranker 怎么选?
- 什么是 Contextual Retrieval?它解决什么问题?
- Chunking、索引、过滤、重排各自解决什么问题?
- 什么是 Query transformation(HyDE、multi-query、decomposition)?
- 什么是 GraphRAG?什么时候值得上?
- 如何设计一个长期运行 Agent 的记忆系统?写入策略怎么设计?
- 如何评估一个 RAG 系统是否真的有效?RAGAS 等工具怎么用?
- Agentic RAG 和 2-Step RAG 有什么区别?
- 多租户 RAG 系统的隔离怎么做?
1. 什么是短期记忆
短期记忆通常指:
当前会话、当前线程、当前任务在运行中需要保留的工作记忆。
它通常包括:
- 最近几轮消息
- 当前任务状态
- 已完成步骤
- 上一步工具结果
- 临时中间产物
短期记忆的特点是:
- 生命周期较短
- 强依赖当前任务
- 对连续执行很重要
LangChain / LangGraph 官方文档通常把这类内容称为 state 或 short-term memory,承载在 checkpointer(MemorySaver、PostgresSaver 等)里。
2. 什么是长期记忆
长期记忆通常指:
跨会话、跨线程、跨时间仍然有复用价值的信息。
例如:
- 用户偏好
- 历史决策
- 组织结构信息
- 某类任务的稳定结论
- 长期积累的业务事实
它的特点是:
- 生命周期更长
- 不一定在每次调用里都要出现
- 需要按需检索
3. 为什么历史对话不等于记忆系统
很多初学者会觉得:
“我把聊天记录全存起来,不就有记忆了吗?”
这只解决了”保存”,没有解决”可用”。
真正的记忆系统至少还要解决:
- 哪些内容值得长期保存(写入门槛)
- 如何去重和合并
- 如何按需检索
- 过时信息怎么遗忘
- 冲突信息怎么处理
所以:
对话日志是原材料,不是成熟的记忆系统。
4. 什么是 RAG
RAG 是 Retrieval-Augmented Generation,通常翻译为”检索增强生成”。
它的核心思想是:
- 先从外部知识源召回相关信息
- 再把这些信息提供给模型
- 最后让模型基于这些信息生成答案
RAG 之所以重要,是因为大模型有两个天然限制:
- 训练知识是静态的
- 上下文窗口是有限的(而且还会退化,见第 3 章的 context rot)
5. RAG 里的几个核心概念
5.1 Retrieval
“找到相关信息”的过程。RAG 的第一步。
5.2 Chunking
大文档不能直接整篇送进向量库或模型,因此要先切块。
切块目标是平衡两件事:
- 每块足够完整,保留语义
- 每块不要太大,便于精确召回
5.3 Embedding
把文本映射成向量表示。向量检索基于这些向量做语义相似搜索。
5.4 Rerank
初步召回的结果不一定最适合最终回答。Rerank 在召回后再做一次更细的排序,提升前几条结果的质量。
常用 reranker 类型:
- Cross-encoder:BGE-reranker、Jina rerank 等开源方案,本地部署友好
- 商业 rerank API:Cohere Rerank v3、Voyage rerank,效果通常更好但有延迟/成本
- LLM-as-reranker:直接让小 LLM 对 top-k 做 pairwise 或 listwise 排序,灵活但贵
- ColBERT / late interaction:token 级交互,质量高但工程复杂
生产上最常见的组合:hybrid 召回 top-100 → cross-encoder rerank 到 top-10。
5.5 Citation
很多生产 RAG 系统不只要回答,还要给出处。检索片段必须带 source metadata(doc_id、url、page、section、权限标签),否则没法做引用、过滤和审计。
6. 为什么工业级 RAG 不能只靠纯向量检索
纯向量检索擅长语义相似,但它有几个天然短板:
- 对精确关键词不稳定
- 对编号、版本号、报错码、SKU、法律条文编号不够敏感
- 容易召回”意思差不多但不是你要的”内容
这就是为什么工业界几乎都需要 Hybrid Search。
7. Hybrid Search
Hybrid Search 就是把:
- 稀疏检索(BM25 关键词检索)
- 稠密检索(向量检索)
结合起来使用。
现实查询通常同时包含两类信号:
- 语义信号
- 字面信号
举个例子:
“为什么订单 ORD-9281 的状态是 DELIVERED,但退款单 RF-201 还没关闭?”
ORD-9281 / DELIVERED / RF-201 都是强字面信号。如果只靠向量检索,容易召回泛化的退款说明文档。加上 BM25 或关键词过滤,命中稳定得多。
合并策略常用 RRF (Reciprocal Rank Fusion):不依赖分数归一化,按两个排序列表的排名合并,工程上极简单且鲁棒。
8. Contextual Retrieval:让 chunk 带上上下文再嵌入
这是 Anthropic 在 2024 年 9 月公开的方法,已成为高质量 RAG 的事实基线。
8.1 解决的问题
传统 chunking 会丢失上下文。例如一段写着”营收增长了 3%“的 chunk,脱离所在文档后,模型不知道是哪家公司、哪个季度。
8.2 做法
在 embed 和索引之前,用 LLM 为每个 chunk 生成一段 50-100 token 的上下文简介,然后把”context + 原 chunk”一起送进 embedding 和 BM25 索引。用的 prompt 大意是:
“Please give a short succinct context to situate this chunk within the overall document for the purposes of improving search retrieval.”
8.3 Anthropic 报告的效果
- 仅 Contextual Embeddings:失败率降低 35%(5.7% → 3.7%)
- Contextual Embeddings + Contextual BM25:降低 49%(→ 2.9%)
- 再叠加 rerank:降低 67%(→ 1.9%)
8.4 工程注意
- 为每个 chunk 调一次 LLM 生成 context 会很贵——配合 Anthropic 的 prompt caching 可以把成本压到 1/10 左右(参见第 3 章)
- Context 生成一次即可(离线预处理时做),不是每次查询都跑
- 对已有 RAG 系统来说这是最 ROI 高的升级之一
9. Query Transformation
原始 query 不一定是”最适合检索的那个 query”,常见的改写策略:
- HyDE (Hypothetical Document Embeddings):让模型先假想一份答案,用假想答案的 embedding 去检索,比直接用问题好
- Multi-query:用 LLM 生成多个改写,并行检索后合并去重
- Query decomposition:复杂问题拆成子问题分别检索(特别适合 multi-hop)
- Step-back question:抽象问题到更高层再检索背景知识
这些都是”用 LLM 的成本换检索质量”的取舍,在高价值场景(法律、医疗、客服知识库)值得开启。
10. GraphRAG:什么时候值得上
Microsoft 2024 年发布的 GraphRAG 把文档组织成实体-关系图,并为社区生成 hierarchical summary,适合回答”全局性问题”(“整个文档集中关于 X 的主要观点是什么”),而不是”局部事实”(“X 在第几页说了什么”)。
工程取舍:
- 上 GraphRAG 的前提:你的问题确实是 cross-document synthesis 型,不是 lookup 型
- 成本不低:构建图和 community summary 需要对整个语料跑多次 LLM
- 增量更新难:文档变了要重算受影响的子图
- 多数企业场景不需要:普通 hybrid + rerank + contextual retrieval 已经够用
11. RAG 的几种典型模式
11.1 2-Step RAG
最经典的模式:检索 → 生成。
优点:简单、成本低、延迟可控。 适合:FAQ、企业知识问答、文档助手。
11.2 Agentic RAG
模型不只是被动接收检索结果,而是能主动决定:
- 先查哪个数据源
- 是否要二次检索
- 是否要换关键词
- 是否需要调用其他工具
优点:灵活、适合复杂任务。 缺点:成本高、更难控、更依赖评估和 guardrails。
11.3 Self-querying / Metadata routing
让 LLM 先解析用户 query 生成结构化过滤条件(例如”2024 年 Q3 之后的财报”→ date >= 2024-10-01),再带过滤执行向量检索。对时间、作者、状态这类结构化维度特别有用。
12. 长期记忆的存储设计
实用分法是三层:
12.1 事实型记忆
例如:用户偏好、固定配置、身份和组织关系。 适合结构化存储:KV、SQL、文档存储。
12.2 情节型记忆
例如:某次任务做了什么、出现了什么问题、最终怎么解决。 适合文档块 + 时间索引 + 向量检索。
12.3 关系型记忆
例如:用户属于哪个项目组、某服务依赖哪个系统、某审批流程需要哪些人参与。 适合图数据库或至少维护关系表。
LangGraph 官方把”long-term memory”抽成 store,可以挂不同 backend(Postgres、Redis 等)。这是一个实用抽象:让 runtime 不关心记忆的具体存储。
13. 长期记忆的更新策略:写入门槛才是关键
不是每轮对话都应该无脑入库。生产系统里写入策略往往比读取策略更重要。
常见做法:
- 候选记忆抽取:LLM 从对话里抽出”可能值得记住的事实”
- 价值判断 / write gate:用分类模型或 LLM-judge 判断是否真的值得入库(大多数对话不值得)
- 去重:向量相似度 + 字段级比对
- 合并:同主题多条 → 一条稳定版本
- 打分:重要性、置信度、时效性
- 入库:带 provenance(来源消息 id、时间)
13.1 写入模式
- Hot-path write:对话中实时写入。延迟低但容易污染记忆库
- Background reflection:异步 job 定期回顾最近对话决定写什么(Reflexion、Generative Agents 里用的方式)
- Event-driven write:特定事件(任务完成、审批通过、用户明确要求记住)才触发写入
生产上建议以 event-driven + background reflection 为主,hot-path 只用于用户明确要求的”请记住…”。
13.2 遗忘机制
长期记忆如果没有遗忘和降权,会越来越脏。建议至少考虑:
- 过时信息降权(
decay(age)) - 低命中记忆降权(长期没被召回就边缘化)
- 低置信度记忆待确认(标记
needs_verification) - 冲突记忆保留版本和时间戳(不要覆盖,追加)
14. RAG 系统的方法论
14.1 先确认知识源
不要一上来就”全量建向量库”。先判断你的知识来自哪里:
- PDF / 文档系统 / Wiki
- SQL / CRM / API
- 代码库 / Issue tracker
很多时候最好的方案不是重建知识库,而是直接把已有系统作为 Tool——让 Agent 调 SQL 或 API,比提前抽取索引更新鲜、更精确。
14.2 先做 Retrieval Baseline
实用步骤是先做最简单可测的 baseline:
- 文档切块
- 建索引
- top-k 召回
- 模型整合回答
然后再逐步加入:
- metadata filter
- hybrid search(向量 + BM25 + RRF)
- rerank(cross-encoder 或商业 API)
- contextual retrieval(Anthropic 方案)
- query rewrite / HyDE / multi-query
- multi-hop retrieval
每加一层都要跑评估,确认真的有提升。
14.3 切块策略要按文档类型设计
不同文档类型的切块策略应该不同:
- 法律合同:按条款和标题切,保留层级
- 技术文档:按章节和代码块切,不要切断代码
- FAQ:按问答对切
- 聊天记录:按轮次或主题切
- 代码:按函数/类切,保留 import 和签名
不要一套固定 chunk size 打天下。通用的参考范围:500-1500 tokens + 10-20% overlap。
14.4 给每个 chunk 补 metadata
尽量保留:
- 标题、来源、时间、文档类型、作者、版本、权限标签
- 如果做了 contextual retrieval,保留 generated context 字段
这些字段对过滤、排序和多租户隔离都很重要。
15. TypeScript 示例:Hybrid + Rerank 的检索编排
type SearchResult = {
id: string;
content: string;
contextualHeader?: string; // 来自 contextual retrieval
source: string;
metadata: Record<string, unknown>;
lexicalScore?: number;
semanticScore?: number;
finalScore?: number;
};
export async function retrieveKnowledge(
query: string,
opts: {
topK?: number;
rerankTopK?: number;
filters?: Record<string, unknown>;
workspaceId: string; // 强制多租户过滤
}
): Promise<SearchResult[]> {
const recallK = 100;
const topK = opts.rerankTopK ?? 10;
const baseFilter = {
...opts.filters,
workspace_id: opts.workspaceId, // 一定要在存储层强制
};
const [vectorResults, keywordResults] = await Promise.all([
semanticSearch(query, recallK, baseFilter),
keywordSearch(query, recallK, baseFilter),
]);
const fused = rrfMerge(vectorResults, keywordResults);
const reranked = await rerank(query, fused.slice(0, 50));
return reranked.slice(0, topK);
}
function rrfMerge(a: SearchResult[], b: SearchResult[]): SearchResult[] {
const k = 60;
const scores = new Map<string, { item: SearchResult; score: number }>();
const add = (list: SearchResult[]) => {
list.forEach((item, rank) => {
const prev = scores.get(item.id);
const add = 1 / (k + rank + 1);
if (prev) {
prev.score += add;
} else {
scores.set(item.id, { item, score: add });
}
});
};
add(a);
add(b);
return [...scores.values()]
.sort((x, y) => y.score - x.score)
.map((entry) => ({ ...entry.item, finalScore: entry.score }));
}
declare function semanticSearch(
q: string,
k: number,
f: Record<string, unknown>
): Promise<SearchResult[]>;
declare function keywordSearch(
q: string,
k: number,
f: Record<string, unknown>
): Promise<SearchResult[]>;
declare function rerank(q: string, r: SearchResult[]): Promise<SearchResult[]>;几个工程要点体现在这段代码里:
- Hybrid 召回用 RRF 融合,不依赖分数归一化
- 多租户过滤在存储层强制,不依赖 app 层记得传
- Rerank 前先用 RRF 筛到合理规模(50-100)
- 返回前 topK(通常 5-10),避免上下文爆炸
16. 如何评估 RAG 是否有效
不能只看”模型回答看起来不错”。建议分三层评估。
16.1 检索层(retriever-level)
- Recall@k、Precision@k
- MRR / nDCG
- 空召回率
构造评估集:从真实流量里采样 query → 人工标注 gold chunks。规模 50-200 条起。
16.2 生成层(generator-level)
- Faithfulness:回答是否被证据支持(有没有幻觉)
- Answer relevance:回答是否回应了用户问题
- Context precision / recall:检索到的 chunks 是否真的被用上
开源工具链:
- RAGAS:最主流的 RAG evals 框架,实现上面这些指标
- TruLens:feedback function 框架,评估更灵活
- LangSmith Evals:和 LangChain 生态集成紧
- Phoenix (Arize):开源 LLM observability + evals
16.3 业务层
- 是否解决用户问题
- 是否降低人工成本
- 是否缩短处理时间
- 是否减少升级工单数
业务指标才是决定 RAG 是否真的”有用”的最终标准。
17. 多租户 RAG 的隔离
如果 Agent 系统是多租户 SaaS,RAG 层必须做硬隔离:
- 存储层强制过滤:索引写入时必须带
workspace_id/tenant_id,查询时必须带 filter,不能只靠 app 层记得传 - Per-tenant namespace:pgvector 可以用 schema 或 partition,Pinecone/Weaviate 用 namespace
- 权限级 chunk filter:不只是租户,还有租户内的 ACL(某文档只有部门 X 能读)
- 检索返回前二次校验:返回的 chunks 如果 metadata 与当前 principal 权限不符,丢弃并报警
- 审计:检索日志必须能回答”谁在什么时间检索了哪些 chunks”
这套隔离在 SaaS 里是合规红线,不是 nice-to-have。
18. 本章方法论小结
- 历史对话不等于成熟的记忆系统
- 短期记忆服务当前任务,长期记忆服务跨会话复用;写入门槛比读取能力更关键
- RAG 的核心是”在生成前拿到更对的外部信息”
- 工业级 RAG 通常是 hybrid retrieval(向量+BM25+RRF)+ rerank + contextual retrieval 三件套
- Query transformation(HyDE / multi-query / decomposition)在高价值场景值得上
- GraphRAG 适合 cross-document synthesis 型问题,大多数场景不需要
- 不同文档类型需要不同 chunking 策略,chunk metadata 必须完整
- 长期记忆需要抽取、合并、去重、降权、遗忘;写入以 event-driven + background reflection 为主
- RAG 评估必须分检索层 / 生成层 / 业务层,用 RAGAS / TruLens / LangSmith evals 自动化
- 多租户隔离必须在存储层强制,不依赖 app 层规矩
- 最好的知识接入方式不一定是建向量库,也可能是把已有数据系统当 Tool